/*
 * Copyright 2020 damios
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at:
 * https://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
//Note, the above license and copyright applies to this file only.

package com.libgdxbar.pvzlikegame.lwjgl3;

import org.lwjgl.system.macosx.LibC;

import java.io.BufferedReader;
import java.io.File;
import java.io.InputStreamReader;
import java.lang.management.ManagementFactory;
import java.util.ArrayList;

/**
 * Adds some utilities to ensure that the JVM was started with the
 * {@code -XstartOnFirstThread} argument, which is required on macOS for LWJGL 3
 * to function. Also helps on Windows when users have names with characters from
 * outside the Latin alphabet, a common cause of startup crashes.
 * <br>
 * <a href="https://jvm-gaming.org/t/starting-jvm-on-mac-with-xstartonfirstthread-programmatically/57547">Based on this java-gaming.org post by kappa</a>
 *
 * @author damios
 */
public class StartupHelper {

	private static final String JVM_RESTARTED_ARG = "jvmIsRestarted";

	private StartupHelper() {
		throw new UnsupportedOperationException();
	}

	/**
	 * Starts a new JVM if the application was started on macOS without the
	 * {@code -XstartOnFirstThread} argument. This also includes some code for
	 * Windows, for the case where the user's home directory includes certain
	 * non-Latin-alphabet characters (without this code, most LWJGL3 apps fail
	 * immediately for those users). Returns whether a new JVM was started and
	 * thus no code should be executed.
	 * <p>
	 * <u>Usage:</u>
	 *
	 * <pre><code>
	 * public static void main(String... args) {
	 * 	if (StartupHelper.startNewJvmIfRequired(true)) return; // This handles macOS support and helps on Windows.
	 * 	// after this is the actual main method code
	 * }
	 * </code></pre>
	 *
	 * @param redirectOutput whether the output of the new JVM should be rerouted to the
	 *                       old JVM, so it can be accessed in the same place; keeps the
	 *                       old JVM running if enabled
	 * @return whether a new JVM was started and thus no code should be executed
	 * in this one
	 */
	public static boolean startNewJvmIfRequired(boolean redirectOutput) {
		String osName = System.getProperty("os.name").toLowerCase();
		if (!osName.contains("mac")) {
			if (osName.contains("windows")) {
// Here, we are trying to work around an issue with how LWJGL3 loads its extracted .dll files.
// By default, LWJGL3 extracts to the directory specified by "java.io.tmpdir", which is usually the user's home.
// If the user's name has non-ASCII (or some non-alphanumeric) characters in it, that would fail.
// By extracting to the relevant "ProgramData" folder, which is usually "C:\ProgramData", we avoid this.
				System.setProperty("java.io.tmpdir", System.getenv("ProgramData") + "/libGDX-temp");
			}
			return false;
		}

		// There is no need for -XstartOnFirstThread on Graal native image
		if (!System.getProperty("org.graalvm.nativeimage.imagecode", "").isEmpty()) {
			return false;
		}

		long pid = LibC.getpid();

		// check whether -XstartOnFirstThread is enabled
		if ("1".equals(System.getenv("JAVA_STARTED_ON_FIRST_THREAD_" + pid))) {
			return false;
		}

		// check whether the JVM was previously restarted
		// avoids looping, but most certainly leads to a crash
		if ("true".equals(System.getProperty(JVM_RESTARTED_ARG))) {
			System.err.println(
				"There was a problem evaluating whether the JVM was started with the -XstartOnFirstThread argument.");
			return false;
		}

		// Restart the JVM with -XstartOnFirstThread
		ArrayList<String> jvmArgs = new ArrayList<>();
		String separator = System.getProperty("file.separator");
		// The following line is used assuming you target Java 8, the minimum for LWJGL3.
		String javaExecPath = System.getProperty("java.home") + separator + "bin" + separator + "java";
		// If targeting Java 9 or higher, you could use the following instead of the above line:
		//String javaExecPath = ProcessHandle.current().info().command().orElseThrow();

		if (!(new File(javaExecPath)).exists()) {
			System.err.println(
				"A Java installation could not be found. If you are distributing this app with a bundled JRE, be sure to set the -XstartOnFirstThread argument manually!");
			return false;
		}

		jvmArgs.add(javaExecPath);
		jvmArgs.add("-XstartOnFirstThread");
		jvmArgs.add("-D" + JVM_RESTARTED_ARG + "=true");
		jvmArgs.addAll(ManagementFactory.getRuntimeMXBean().getInputArguments());
		jvmArgs.add("-cp");
		jvmArgs.add(System.getProperty("java.class.path"));
		String mainClass = System.getenv("JAVA_MAIN_CLASS_" + pid);
		if (mainClass == null) {
			StackTraceElement[] trace = Thread.currentThread().getStackTrace();
			if (trace.length > 0) {
				mainClass = trace[trace.length - 1].getClassName();
			} else {
				System.err.println("The main class could not be determined.");
				return false;
			}
		}
		jvmArgs.add(mainClass);

		try {
			if (!redirectOutput) {
				ProcessBuilder processBuilder = new ProcessBuilder(jvmArgs);
				processBuilder.start();
			} else {
				Process process = (new ProcessBuilder(jvmArgs))
					.redirectErrorStream(true).start();
				BufferedReader processOutput = new BufferedReader(
					new InputStreamReader(process.getInputStream()));
				String line;

				while ((line = processOutput.readLine()) != null) {
					System.out.println(line);
				}

				process.waitFor();
			}
		} catch (Exception e) {
			System.err.println("There was a problem restarting the JVM");
			e.printStackTrace();
		}

		return true;
	}

	/**
	 * Starts a new JVM if the application was started on macOS without the
	 * {@code -XstartOnFirstThread} argument. Returns whether a new JVM was
	 * started and thus no code should be executed. Redirects the output of the
	 * new JVM to the old one.
	 * <p>
	 * <u>Usage:</u>
	 *
	 * <pre>
	 * public static void main(String... args) {
	 * 	if (StartupHelper.startNewJvmIfRequired()) return; // This handles macOS support and helps on Windows.
	 * 	// the actual main method code
	 * }
	 * </pre>
	 *
	 * @return whether a new JVM was started and thus no code should be executed
	 * in this one
	 */
	public static boolean startNewJvmIfRequired() {
		return startNewJvmIfRequired(true);
	}
}
